6. 搜索读链路 + 热词沉淀全链路详解
本文档是 SmartLive 项目核心搜索基座的深度拆解,适合面试 10 分钟讲解版本。
涵盖:Elasticsearch 复杂 DSL(组合查询 / 分词高亮 / LBS 地理围栏) → 多维度策略排序(距离/评分/热度权重) → Redis ZSet O(logN) 维护热搜榜单与千人千面搜索历史。
1. 链路总览图
2. 每一步的技术细节与面试追问
Step 1~2:热搜词频与用历史双路沉淀 (Redis ZSet)
做了什么:
当用户发起有效搜索时,异步调用 SearchServiceImpl 内的沉淀接口:
- 千人千面的历史记录:利用 Redis ZSet,把搜索词当做 value、当前时间戳 System.currentTimeMillis() 当做 Score。先
zrem(防重),后zadd(盖上最新时间),最后zremrange(永远只保留 Score 最大的 前10条)。 - 全网热搜风向标:维护一个全局唯一的
SEARCH_HOT_KEYWORDSZSet,调用zincrby使该搜索词的 Score 加 1,轻松扛住上万并发的点击热度排行榜。
面试追问与回答:
| 问题 | 回答 |
|---|---|
| 为什么历史搜索和热搜都要用 Redis ZSet 而不是数据库? | 用户每改个字点一下搜索都会触发写操作,TPS 极高,MySQL 抗写成本太大。ZSet 能利用内存 O(log(N)) 的极速实现按分数自然排序;历史搜索根据“时间戳”拿最近的,热词根据“增量次数”拿最火的,完美契合业务场景。 |
| 如果有人恶意利用脚本无限刷冷门词汇,导致热搜库 OOM 怎么办? | 热搜 ZSet 我们给了 TTL(如 1 小时)。由于只取 Top N 后展示,长尾的冷门词汇虽然存在时间短,只要热度抢不过主流词就会被平滑淘汰出内存。必要时可在 Gateway 进行 IP 级限流。 |
Step 3:ES 复杂查询条件构建 (BoolQueryBuilder)
做了什么:
处理基于 Elasticsearch 的布尔查询(BoolQuery)。
- 全文检索:提取商家的
name、address等多个字段,用MultiMatchQuery统排,实现输入一句话,哪里的字对上都能搜出。 - 范围限制:解析客户端传来的筛选器
maxPrice或minScore,装进RangeQuery进行打分无用的硬性剔除(Filter)。 - LBS 地理围栏:拿到手机所在的
Lat/Lon和用户选择的圈选距离(如 "3km"),使用geoDistanceQuery进行方圆检索。
面试追问与回答:
| 问题 | 回答 |
|---|---|
Elasticsearch 查询里,must 和 filter 的核心区别是什么? | must 是查询匹配,不仅要做倒排判定,还要计算 TF-IDF 或 BM25 相关性算分,比较耗时;filter 是布尔判定(行不行),不参与算分,并且其结果会被 ES 自动缓存在 Node 层级的 Bitset 里。我们在项目中凡是价格区间、距离围栏、固定类别全用 filter 让 ES 提速。 |
| LBS 经纬度查附件的人为什么不用 Redis 的 GEO? | Redis GEO 本质底层还是 ZSet(GeoHash 映射),它只适合极其单纯的找距离。但电商找店铺,除了距离近,你还必须拼上“名字得叫烤肉”、“评分还得 > 4分”、“客单价 < 100”。这种包含字符串分词、多维数值筛选+距离结合的场景,只有 Elasticsearch 的宽表索引(组合查询)能做到一次搞定。 |
Step 4~6:动态策略排序与高亮渲染 (Strategy + Highlight)
做了什么:
- 策略模式拆分排序权重:排序涉及的组合五花八门(距离由近到远且评分由高到底;或者价格最低优先)。利用
ShopSortStrategyFactory配合前端传来的sortBy参数分发到对应的 Strategy 实例。这些策略类自己调用SortBuilders为当前查询源加上不同的尾巴。 - 红色高亮渲染:实例化
HighlightBuilder,遍历所有匹配字段,主动为匹配词挂上<span style='color: red;'>的 HTML 前后缀,实现百度一样的命中词飘红效果。
面试追问与回答:
| 问题 | 回答 |
|---|---|
| 为什么 Elasticsearch 你不把高亮放到前端去正则匹配处理? | 前端无法预知 ES 底层分词器(如 IK 分词)到底把一句话切成了什么碎片!ES 会基于当时倒排索引生成的 Offset 精准圈出匹配命中的位置。交给前端去高亮容易导致语义切分错误,而且前后端代码强耦合。 |
| 高并发下 Elasticsearch 分页如果翻到很深的页,会发生什么? | 会引发经典的深度分页灾难(Deep Paging 问题,返回 From + Size 个 Document 在 Coordinator 节点内存排序爆炸)。我们在实际应用中,要么开启了 max_result_window 的 10000 强阻断,要么使用 Scroll 或 Search_After 来承接瀑布流式的大表深翻。 |
3. 整条链路涉及的核心技术点汇总
┌─────────────────────────────────────────────────────┐
│ 技术点对照表 │
├──────────────┬──────────────────────────────────────┤
│ 热数据挖掘 │ · Redis ZSet incrementScore (热词榜) │
│ │ · Redis ZSet 时间戳排名 (千人千面足迹) │
├──────────────┼──────────────────────────────────────┤
│ 检索算法原理 │ · ES Inverted Index (倒排查询机制) │
│ │ · TF / BM25 文本相关性动态降权打分 │
├──────────────┼──────────────────────────────────────┤
│ 组合查询 DSL │ · Bool Query (Must vs Filter 缓存运用) │
│ │ · Geo-Distance (LBS 经纬度位置计算) │
├──────────────┼──────────────────────────────────────┤
│ 架构设计模式 │ · 工厂+策略模式(消化复杂多参数组合器) │
└──────────────┴──────────────────────────────────────┘4. 面试 10 分钟讲述模板
讲述思路:以极高并发的 Redis 写为引子,深入探讨 ES 严苛复杂的查询重写逻辑与打分优化。
开场(1 分钟)
"在 SmartLive 平台里,我主导了面向 C 端主站的核心搜索架构。搜索绝不仅限于查表,它的本质是 响应用户检索意图时,如何平滑实现『记流水+取高亮+算距离+排好序』的高并发聚合。这套系统由 Redis 实时缓存阵列与 ES 检索引擎双轴驱动。"
实时词频沉淀(2.5 分钟)
"用户的每一次点击回车,我们的拦截器并不往数据库写任何东西,而是极其快速地发出两条 Redis 密电:第一条往该用户的 ZSet 个人抽屉塞入当前毫秒时间戳与 Keyword 确保搜过必排在前列;第二条对全网宏观大池的 ZSet 使用
zincrby命令疯狂+1。不耗费一张 MySQL 表,就能让首页立刻渲染出包含『近期谁在看』与『全站热搜风云榜』的双向金字塔。"
ES 复杂语句组装(3.5 分钟)
"进了 ES 实际查询深水区,核心难点在于不同赛道的打分混排。我利用 BoolQuery 对象将查询严格一分为二:对于用户输入的文字,放入
Must使用MultiMatch执行昂贵的包含中文分词切割的 TF/IDF 打理;对于价格门槛、类别甚至 LBS 当前他所在的经纬度商圈圈选(GeoDistance),统统发配给Filter,绝对不消耗 ES 一丝一毫的相关性算力,直接走缓存 BitSet 拦截海量无效记录。"
策略路由与端到端渲染(2 分钟)
"因为有了价格最低、离我最近、评分最高这几把不同维度的尺子,如果是传统的 if-else 拼排序条件代码早已惨不忍睹。我剥离出策略工厂动态挂载 SortBuilder,在离开网关前,利用 Highlight API 给命中词完美封边包裹 HTML 高亮标签。当几万条商铺从 ES 吐回前端后,呈现的便是一笔带有准确命中飘红、按他喜好定制排序好的精准结果流。"
总结亮点(1 分钟)
"这条搜索链路的亮点不仅是技术的无脑堆叠,而是:知晓 ZSet 对于排序流水的天然降维打击,洞察 ES Filter 免查缓存的提速奥义,以及运用封装好设计模式将代码里的各种硬编码过滤消隐于无形。这是做 C 端流量搜索的不二法则。"
5. 高频追问速查表
| 追问方向 | 关键问题 | 核心回答 |
|---|---|---|
| 打分逻辑 | Must 和 Should 在 ES 里怎么配合才能提高搜索准确度? | Must 是绝对门槛,进不来就刷掉;Should 是加分项。假如用户搜『牛肉烤肉』,我们可以 Must 匹配『烤肉』类型,但如果该商铺详情页 Should 命中了『牛肉』,ES 会给这篇文档算分加成,将它在榜单里往上顶。这就叫“既满足底线、又凸显亮点”。 |
| 倒排机制 | 新词横空出世,你们的 ES 词库分不出来怎么办? | 默认的 IK 分词器如果没有词库支持,会把新词切成单字导致搜索范围虚胖。通常我们会定时热更新自维护的 IK 自定义词库(通过一个独立小接口喂新词),让 ES 能连贯认出网络热梗。 |
| Redis 挤压 | 你的个人搜索历史如果不设防,ZSet 无限变大会占完内存的吧? | 每次我们执行 zadd 写入当前时间戳后,会紧紧跟上一条 zremrangeByRank(key, 0, -11) 指令。也就是说,无论他搜了十万次,他在内存中永远只占前十名的那可怜的几十个 Byte,并为这把钥匙锁上过期时间,用极其微薄的内存便实现了海量管理。 |
6. 扩展:这条链路和八股的关联
搜索读取与沉淀链路 ←→ 八股知识点映射
Redis 底层八股引申:
├── ZSet 底层数据结构:跳表(SkipList)在 O(logN) 跨度查找上的精妙应用
├── 并发情况下的写覆盖避免(Redis 的全局单线程与 incr 原语威力)
Elasticsearch 底层八股引申:
├── ES 写入过程与倒排索引(Inverted Index):Segment / Translog 机制
├── 为什么 Filter 走缓存不走打分?(Roaring Bitmap 在 ES 过滤层面的合并加速)
├── 深分页问题怎么背锅(From+Size -> Scroll 游标 -> Search_After 滚动)
高并发与架构解耦八股:
├── 读写分离视角:所有的复杂聚合在读集群 ES 完成,而不给 MySQL 施加表关联(Join)压力
├── 什么是 LBS 服务与 GeoHash 算法在空间检索上的切割地图思想?一句话总结:玩转 C 端大搜,不是生硬地
SELECT LIKE。必须深刻掌握 Redis 极致的排序缓存切分 + ES 宽表降维打击与分层过滤。当你能清晰道出Must=打分、Filter=缓存、Strategy=解耦这个三段论时,搜索架构的面试就已经稳拿高分了。